﻿using System;
using System.Collections.Generic;
using System.Globalization;
using Jayden.Dll.Web.Enums;
using Jayden.Dll.Web.Exceptions;
using Jayden.Dll.Web.HtmlControls.Generics;
using Jayden.Dll.Web.HtmlControls.Interfaces;

namespace Jayden.Dll.Web.HtmlControls.Descriptions
{
	public abstract class HtmlElementDescription : HtmlControlDescription, IComparable<HtmlElementDescription>
	{
		private string m_TagName;
		private bool m_ForceClosingTag = true;
		private bool m_AllowControls = true;
		private HtmlVersion m_Version = HtmlVersion.Html4;

		private static HashSet<string> m_GlobalAttributes;
		private static HashSet<string> m_CoreAttributes;
		private static HashSet<string> m_EventAttributes;
		private static HashSet<string> m_XmlAttributes;

		static HtmlElementDescription()
		{
			m_CoreAttributes = new HashSet<string>(new string[]
			{
				"accesskey",
				"class",
				"contenteditable",
				"contextmenu",
				"dir",
				"draggable",
				"dropzone",
				"hidden",
				"id",
				"lang",
				"spellcheck",
				"style",
				"tabindex",
				"title",
				"translate",
			});
			m_EventAttributes = new HashSet<string>(new string[]
			{
				"onabort",
				"onblur",
				"oncanplay",
				"oncanplaythrough",
				"onchange",
				"onclick",
				"oncontextmenu",
				"ondblclick",
				"ondrag",
				"ondragend",
				"ondragenter",
				"ondragleave",
				"ondragover",
				"ondragstart",
				"ondrop",
				"ondurationchange",
				"onemptied",
				"onended",
				"onerror",
				"onfocus",
				"oninput",
				"oninvalid",
				"onkeydown",
				"onkeypress",
				"onkeyup",
				"onload",
				"onloadeddata",
				"onloadedmetadata",
				"onloadstart",
				"onmousedown",
				"onmousemove",
				"onmouseout",
				"onmouseover",
				"onmouseup",
				"onmousewheel",
				"onpause",
				"onplay",
				"onplaying",
				"onprogress",
				"onratechange",
				"onreadystatechange",
				"onreset",
				"onscroll",
				"onseeked",
				"onseeking",
				"onselect",
				"onshow",
				"onstalled",
				"onsubmit",
				"onsuspend",
				"ontimeupdate",
				"onvolumechange",
				"onwaiting"
			});
			m_XmlAttributes = new HashSet<string>(new string[]
			{
				"xml:lang",
				"xml:space",
				"xml:base"
			});
			m_GlobalAttributes = new HashSet<string>();
			foreach (string core in m_CoreAttributes)
				m_GlobalAttributes.Add(core);
			foreach (string eventHandler in m_EventAttributes)
				m_GlobalAttributes.Add(eventHandler);
			foreach (string xml in m_XmlAttributes)
				m_GlobalAttributes.Add(xml);
		}

		public static bool IsGlobalAttribute(string name) { return m_GlobalAttributes.Contains(name); }

		public HtmlElementDescription(string tagName) : this(tagName, HtmlVersion.Html4, true, true) { }
		public HtmlElementDescription(string tagName, HtmlVersion version) : this(tagName, version, true, true) { }
		public HtmlElementDescription(string tagName, HtmlVersion version, bool forceClosingTag) : this(tagName, version, forceClosingTag, true) { }
		public HtmlElementDescription(string tagName, HtmlVersion version, bool forceClosingTag, bool allowControls)
		{
			if (string.IsNullOrWhiteSpace(tagName))
				throw new NullReferenceException();
			m_TagName = tagName.Trim().ToLowerInvariant();
			m_ForceClosingTag = forceClosingTag;
			m_AllowControls = allowControls;
			m_Version = version;
		}

		public string TagName { get { return m_TagName; } }
		public virtual string PermittedContents { get { return null; } }
		public bool ForceClosingTag { get { return m_ForceClosingTag; } }
		public bool AllowChildControls { get { return m_AllowControls; } }
		public virtual bool AllowFlowContent { get { return AllowChildControls; } }
		public virtual bool OnlyFlowContent { get { return false; } }
		public virtual bool AllowPhrasingContent { get { return AllowFlowContent && AllowInnerText; } }
		public virtual bool OnlyPhrasingContent { get { return false; } }
		public virtual bool IsFlowElement { get { return IsPhrasingElement; } }
		public virtual bool IsPhrasingElement { get { return false; } }
		public virtual bool IsMetaDataElement { get { return false; } }
		public HtmlVersion Version { get { return m_Version; } }

		public string HtmlMarkupUrlW3C { get { return string.Format("http://www.w3.org/TR/html-markup/{0}.html", TagName); } }

		private HashSet<string> m_PermittedAttributes = null;
		private Dictionary<string, HtmlElementDescription> m_PermittedElementsByTagName = null;
		private Dictionary<string, HtmlElementDescription> m_PermittedParentElementsByTagName = null;

		protected void CheckAttributes()
		{
			if (m_PermittedAttributes == null)
			{
				lock (this)
				{
					if (m_PermittedAttributes == null)
					{
						m_PermittedAttributes = new HashSet<string>();
						foreach (string attributeName in PermittedAttributes)
							m_PermittedAttributes.Add(attributeName);
					}
				}
			}
		}
		protected void CheckElements()
		{
			if (m_PermittedElementsByTagName == null)
			{
				lock (this)
				{
					if (m_PermittedElementsByTagName == null)
					{
						m_PermittedElementsByTagName = new Dictionary<string, HtmlElementDescription>();
						foreach (HtmlElementDescription d in PermittedElements)
							m_PermittedElementsByTagName.Add(d.TagName, d);
					}
				}
			}
		}
		protected void CheckParents()
		{
			if (m_PermittedParentElementsByTagName == null)
			{
				lock (this)
				{
					if (m_PermittedParentElementsByTagName == null)
					{
						m_PermittedParentElementsByTagName = new Dictionary<string, HtmlElementDescription>();
						foreach (HtmlElementDescription d in PermittedParents)
							m_PermittedParentElementsByTagName.Add(d.TagName, d);
					}
				}
			}
		}

		public virtual bool AllowAttribute(string name)
		{
			// allow all global attributes
			if(m_GlobalAttributes.Contains(name))
				return true;
			if (m_PermittedAttributes == null && PermittedAttributes == null)
				return true;
			// allow all data- attribute on all elements
			if (name.StartsWith("data-"))
				return true;
			CheckAttributes();
			return m_PermittedAttributes.Contains(name);
		}
		public virtual bool AllowElement(HtmlElementDescription description)
		{
			return AllowTagName(description.TagName);
		}
		public virtual bool AllowTagName(string tagName)
		{
			if (m_PermittedElementsByTagName == null && AllowAllElements)
				return true;
			CheckElements();
			return m_PermittedElementsByTagName.ContainsKey(tagName);
		}
		public bool AllowAllElements { get { return PermittedElements == null; } }
		public virtual bool AllowParentTagName(string tagName)
		{
			if (m_PermittedParentElementsByTagName == null && PermittedParents == null)
				return true;
			CheckParents();
			return m_PermittedParentElementsByTagName.ContainsKey(tagName);
		}
		public virtual bool AllowParentElement(HtmlElementDescription description) { return AllowParentTagName(description.TagName); }
		public virtual bool AllowInnerText { get { return true; } }

		public virtual IEnumerable<string> PermittedAttributes { get { return null; } }
		public virtual IEnumerable<HtmlElementDescription> PermittedElements { get { return null; } }
		public virtual IEnumerable<HtmlElementDescription> PermittedParents { get { return null; } }

		private void CheckAttributeBoolean(HtmlElement element, Type type, string attributeName) { CheckAttribute(element, type, attributeName, attributeName); }
		private void CheckAttributeType(HtmlElement element, Type dataType, string attributeName, bool required = false)
		{
			string value = element[attributeName];
			if (value == null)
			{
				if (required)
					throw new HtmlException("invalid missing value for required attribute '{0}' of <{1}>.", attributeName, element.TagName);
				return;
			}
			uint vUint;
			int vInt;
			double vDouble;
			bool vBool;
			if (dataType == typeof(uint) && !uint.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out vUint))
				throw new HtmlException("invalid uint value '{2}' for attribute '{0}' of <{1}>.", attributeName, element.TagName, value);
			if (dataType == typeof(int) && !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out vInt))
				throw new HtmlException("invalid int value '{2}' for attribute '{0}' of <{1}>.", attributeName, element.TagName, value);
			if (dataType == typeof(double) && !double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out vDouble))
				throw new HtmlException("invalid double value '{2}' for attribute '{0}' of <{1}>.", attributeName, element.TagName, value);
			if (dataType == typeof(bool) && !bool.TryParse(value, out vBool))
				throw new HtmlException("invalid bool value '{2}' for attribute '{0}' of <{1}>.", attributeName, element.TagName, value);
		}
		private void CheckAttribute(HtmlElement element, Type type, string attributeName, params string[] possibleValues)
		{
			if (possibleValues == null || possibleValues.Length == 0)
				throw new ArgumentException("Should not be empty.", "possibleValues");
			string value = element[attributeName];
			if (value == null)
				return;
			bool couldBePresent = type.IsAssignableFrom(element.GetType());
			if (!couldBePresent)
				throw new Exceptions.HtmlException("{0} does not allow attribute '{1}' as it does not implement interface {2}.", element.Description, attributeName, type.Name);
			foreach (string possibleValue in possibleValues)
				if (value.Equals(possibleValue))
					return;
			throw new Exceptions.HtmlException("attribute '{0}' of {1} is '{2}' and should be one of the following : null,'{3}'.", attributeName, element.Description, value, string.Join("','", possibleValues));
		}

		public override bool CheckValidity(HtmlControl control)
		{
			if (base.CheckValidity(control))
			{
				HtmlElement element = control as HtmlElement;
				if (element == null)
					throw new Exception(string.Format("control should be of type '{0}'.", typeof(HtmlElement)));
				if (element.TagName != TagName)
					throw new Exception(string.Format("invalid tag, should be {0} and is <{1}>.", this, element.TagName));
				if (element.Parent != null && !AllowParentElement(element.Parent.Description as HtmlElementDescription))
					throw new Exception(string.Format("invalid parent tag, {0} cannot be contained inside <{1}>.", this, element.TagName));
				foreach (string attributeName in element.Attributes)
					if (!AllowAttribute(attributeName))
						throw new Exceptions.HtmlException("{0} does not allow attribute '{1}'.", this, attributeName);
				CheckAttributeBoolean(element, typeof(WithRequired), "required");
				CheckAttributeBoolean(element, typeof(WithMultiple), "multiple");
				CheckAttributeBoolean(element, typeof(WithDisabled), "disabled");
				CheckAttributeBoolean(element, typeof(WithAutoFocus), "autofocus");
				CheckAttributeBoolean(element, typeof(WithReadOnly), "readonly");
				CheckAttributeBoolean(element, typeof(WithChecked), "checked");
				CheckAttributeBoolean(element, typeof(WithSelected), "selected");
				CheckAttributeBoolean(element, typeof(WithAsync), "async");
				CheckAttributeBoolean(element, typeof(WithDefer), "defer");
				CheckAttributeBoolean(element, typeof(WithAutoPlay), "autoplay");
				CheckAttributeBoolean(element, typeof(WithControls), "controls");
				CheckAttributeBoolean(element, typeof(WithMuted), "muted");
				CheckAttributeBoolean(element, typeof(WithLoop), "loop");
				CheckAttributeBoolean(element, typeof(WithOpen), "open");
				CheckAttribute(element, typeof(WithAutoComplete), "autocomplete", "on", "off");
				if (m_AllowControls)
				{
					foreach (HtmlControl child in element.ChildControls)
					{
						if (child is HtmlElement)
						{
							bool validated = false;
							HtmlElement childElt = child as HtmlElement;
							bool allow = AllowElement(childElt.Description);
							if (childElt.Description.IsFlowElement)
							{
								if (!AllowFlowContent)
									throw new Exception(string.Format("<{0}> is a flow element, <{1}> does not allow flow element.", childElt.TagName, element.TagName));
								else
									validated = true;
							}
							else if (OnlyFlowContent && !allow)
								throw new Exception(string.Format("<{0}> is not a flow element, <{1}> only allow flow element.", childElt.TagName, element.TagName));
							if (childElt.Description.IsPhrasingElement)
							{
								if (!AllowPhrasingContent)
									throw new Exception(string.Format("<{0}> is a phrasing element, <{1}> does not allow phrasing element.", childElt.TagName, element.TagName));
								else
									validated = true;
							}
							else if (OnlyPhrasingContent && !allow)
								throw new Exception(string.Format("<{0}> is not a phrasing element, <{1}> only allow phrasing element.", childElt.TagName, element.TagName));
							if (!validated && !allow)
								throw new Exception(string.Format("<{0}> is not allowed in <{1}>.", childElt.TagName, element.TagName));
						}
						else if (child is HtmlText && !AllowInnerText)
							throw new Exception(string.Format("<{0}> cannot contains text.", element.TagName));
						if (child.Description != null)
							child.Description.CheckValidity(child);
					}
				}
				return true;
			}
			return false;
		}

		public override string ToString() { return string.Format("<{0}>", m_TagName); }

		public int CompareTo(HtmlElementDescription other) { return m_TagName.CompareTo(other.m_TagName); }
	}
}
